package com.pic.mgt.monitor;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import com.pic.mgt.log.IgxLog;
import com.pic.mgt.monitor.backup.UnitBackup;
import com.pic.mgt.monitor.parser.ParseToUnitException;
import com.pic.mgt.monitor.parser.ParseToUnitFormatException;
import com.pic.mgt.monitor.parser.UnitHandler;
import com.pic.mgt.monitor.physicalmonitor.FileMonitor;
import com.pic.mgt.monitor.physicalmonitor.FileMonitorException;
import com.pic.mgt.monitor.physicalmonitor.FileMonitorStopException;
import com.pic.mgt.monitor.physicalmonitor.JPathWatchFileMonitor;
import com.pic.mgt.monitor.physicalmonitor.MonitoredFile;
import com.pic.mgt.monitor.physicalmonitor.FileMonitor.FileMonitorEvent;
import com.pic.mgt.monitor.physicalmonitor.FileMonitor.FileMonitorStatus;
import com.pic.mgt.queue.QueueService;
import com.pic.mgt.unit.Unit;
import com.pic.mgt.utils.IgxFileUtils;
import com.pic.mgt.utils.RunTimeSystemUtils;

/**
 * [Abstract class 單元監控]
 * @author 洪政緯
 *
 */
public final class SimpleUnitMonitor implements Runnable, UnitMonitor {
	private static String haishinOutboxPathStr;
	private UnitBackup physicalUnitBackup;
	private UnitMonitorListener unitMonitorListener;
	private FileMonitor fileMonitor;
	private int retryInterval;
	private int retryTimesLimit;
	private boolean ignoreEmptyFile;
	private List<FilenameFilter> filenameFilterList;  
	
	private UnitHandler unitHandler;
	
	// 處理將要分析的 Monitored File List
	private MonitoredFileListHandler monitoredFileListHandler;
	// 處理 Monitored File 的 Retry
	private RetryMonitoredFileHandler retryMonitorFileHandler;
	
	private final FileMonitorEvent monitorEvent = 
										FileMonitorEvent.FILE_Generate;
	
	public SimpleUnitMonitor(UnitHandler unitHandler,
							  FileMonitor fileMonitor) {
		this.unitHandler = unitHandler;
		this.fileMonitor = fileMonitor;	
		
		// 建立 Monitored File List 處理
		this.monitoredFileListHandler = new MonitoredFileListHandler();
		
		// 建立 Retry Monitored File Queue 重試機制
		this.retryMonitorFileHandler = new RetryMonitoredFileHandler();	
		
		filenameFilterList = new LinkedList<FilenameFilter>();
		
		this.fileMonitor.setMonitorEvent(monitorEvent);
	}
	
	@Override
	public synchronized boolean isUnitMonitorRunning() {
		return 
			isFileMonitorRunning();
	}
	
	@Override	
	public synchronized void setUnitMonitorListener(UnitMonitorListener listener) {
		// 在還未開始監控的情況下,才能夠設定
		if (!isFileMonitorRunning()){
			this.unitMonitorListener = listener;
		}		
	}
	
	@Override
	public synchronized void removeUnitMonitorListener() {
		// 在還未開始監控的情況下,才能移除
		if (!isFileMonitorRunning()){
			unitMonitorListener = null;
		}		 
	}
	
	@Override
	public synchronized void setMonitorPath(String ... monitorPath) throws UnitMonitorException {
		for (String path : monitorPath) {
			addMonitorPath(path);
		}		
	}
	
	@Override
	public synchronized void addMonitorPath(String monitorPath) throws UnitMonitorException {
		try {
			// throws FileMonitorException
			this.fileMonitor.addMonitorPath(monitorPath);
			
			// ********************** Log **********************
			if (IgxLog.messageLog.isDebugMode()) {
				IgxLog.messageLog.debug(this,
						"Added a unit monitor path '{}'.", monitorPath);
			}
			// ********************** Log **********************
		} catch (FileMonitorException e) {
			try {
				throw new UnitMonitorException(
						"SUDA-267 Adding a unit monitor path '" + monitorPath + "' failed.", e);
			} catch (UnitMonitorException e1) {
				// ********************** Log **********************
				IgxLog.messageLog.error(this, e1.getMessage());
				// ********************** Log **********************
				
				throw e1;
			}
		}
	}
	
	@Override
	public synchronized void removeAllMonitorPath() {
		if (null != this.fileMonitor) {
			this.fileMonitor.removeAllFilenameFilter();
		}
	}
	
	@Override
	public synchronized void removeMonitorPath(String monitorPathStr) {
		if (null != this.fileMonitor) {
			this.fileMonitor.removeMonitorPath(monitorPathStr);
		}
	}
	
	@Override
	public synchronized void startWatchMonitorPath(String monitorPathStr) throws UnitMonitorException {
		if (null != this.fileMonitor) {
			try {
				this.fileMonitor.startWatchMonitorPath(monitorPathStr);
			} catch (FileMonitorException e) {
				try {
					throw new UnitMonitorException(
							"SUDA-268 Setting monitor path '" + monitorPathStr + "' watch failed.", e );
				} catch (UnitMonitorException e1) {
					// ********************** Log **********************
					IgxLog.messageLog.error(this, e1.getMessage());
					// ********************** Log **********************
					
					throw e1;
				}
			}
		}
	}
	
	@Override
	public synchronized void stopAllMonitorPath() {
		if (null != this.fileMonitor) {
			this.fileMonitor.stopWatchAllMonitorPath();
		}
	}
	
	@Override
	public synchronized void stopWatchMonitorPath(String monitorPathStr) {
		if (null != this.fileMonitor) {
			this.fileMonitor.stopWatchMonitorPath(monitorPathStr);
		}
	}
	
	@Override
	public synchronized void setFilenameFilter(FilenameFilter ... filenameFilter) {
		// 在還未開始監控的情況下,才能夠設定
		if (!isFileMonitorRunning()) {
			for(FilenameFilter filter : filenameFilter) {
				addFilenameFilter(filter);
			}
		}
	}
	
	@Override
	public synchronized void addFilenameFilter(FilenameFilter filenameFilter) {
		// 在還未開始監控的情況下,才能夠設定
		if (!isFileMonitorRunning()) {
			synchronized (this.filenameFilterList) {
				this.filenameFilterList.add(filenameFilter);
			}
			this.fileMonitor.addFilenameFilter(filenameFilter);
		}
	}
	
	@Override
	public synchronized void removeAllFilenameFilter() {
		// 在還未開始監控的情況下,才能夠移除
		if (!isFileMonitorRunning()) {
			this.fileMonitor.removeAllFilenameFilter();
			synchronized (this.filenameFilterList) {
				this.filenameFilterList.clear();
			}
		}
	}
	
	@Override
	public synchronized void startUnitMonitor() throws UnitMonitorException {
		if (!isFileMonitorRunning()) {
			try {
				// 開始 Monitored File List Queue 處理
				monitoredFileListHandler.startQueueService();
				
				// 開始  Retry Monitored File Queue 重試機制
				retryMonitorFileHandler.startQueueService();
				
				/*
				 * 開始檔案監控
				 * throw FileMonitorDefineException
				 */				
				fileMonitor.startMonitor();
				
				// 取得被監控到的檔案				
				new Thread(this,"Business Monitor").start();
				
				// ********************** Log **********************
				if (IgxLog.messageLog.isInfoMode()) {
					IgxLog.messageLog.info(this, "Started unit monitor.");
				}
				// ********************** Log **********************
				
			} catch (FileMonitorException e) {
				/*
				 * 發生 FileMonitorException 有二種情況:
				 * (1) 在建立底層檔案監控元件時發生的錯誤，此時完全無法監控(監控狀態也不會是在 running 的狀態)。
				 * (2) 建立底層檔案監控元件成功，但在加入目錄監控時發生錯誤(有可能是目錄不存在或其它狀況等)，此
				 * 		時要注意的是監控狀態會是在 running ，並且先前有加入目錄監控成功的目錄應是正常在監控的。
				 */
				try {
					if (!isFileMonitorRunning()) {
						monitoredFileListHandler.stopQueueService();
						monitoredFileListHandler = null;
						
						retryMonitorFileHandler.stopQueueService();
						retryMonitorFileHandler = null;
						
						throw new UnitMonitorException(
								"Start unit monitor failed.", e );
					}
					else {
						// 監控仍正常開始，某目錄加入監控失敗。
						throw new UnitMonitorException(
								"SUDA-269 Started unit monitor. But some folders adding to monitor failed.", e );
					}
				} catch (UnitMonitorException e1) {
					// ********************** Log **********************
					IgxLog.messageLog.error(this, e1.getMessage());
					// ********************** Log **********************
					
					throw e1;
				}
			}
		}
	}
	
	/*
	 * stop 機制並不會立刻Stop。 
	 * 會等  Monitored File List Queue 和 Retry Monitored File Queue
	 * 內的資料都被處理完後，才會Stop.
	 */
	@Override
	public synchronized void stopUnitMonitor() {
		if (!isFileMonitorRunning()) {
			return;
		}
		
		/* 先停止 FileMonitor，讓檔案監控的資料不會再進來。
		 * (呼叫 stopMonitor() 會使呼叫readMonitoredList()方法
		 * 而在等待中的thread產生 FileWatchServiceStopException例外)
		 */
		if (null != fileMonitor) {
			fileMonitor.stopMonitor();
		}
		
		/*
		 * 停止 Monitored File Retry。
		 * 會停在此方法，直到 Retry Monitored File Handler 內的資料被處理完，
		 * 結束並反回。
		 */
		if (this.retryMonitorFileHandler.isWorking()) {
			this.retryMonitorFileHandler.stopQueueService();
		}
		
		/*
		 * 停止 Monitored File Queue 處理
		 * 會停在此方法，直到 Monitored File Handler 內的資料被處理完，
		 * 結束並反回。
		 */
		if (this.monitoredFileListHandler.isWorking()) {
			monitoredFileListHandler.stopQueueService();
		}
		
		// ********************** Log **********************
		if (IgxLog.messageLog.isInfoMode()) {
			IgxLog.messageLog.info(this, "Stopped unit monitor.");
		}
		// ********************** Log **********************
	}
	
	private synchronized boolean isFileMonitorRunning() {
		boolean running;
		if (fileMonitor != null &&
				fileMonitor.getMonitorStatus() == FileMonitorStatus.MONITOR_STARTED){
			running = true;
		}
		else {
			running = false;
		}
		return running;
	}
	
	public void run() {
		/*
		 * 本 thread 只負責接收到 monitored File
		 * 並將 monitored file list 放入 Monitored File List Queue
		 */
		try {
			while(true) {
				// 程式呼叫 readMonitoredList() 會停在此方法,一直到有資料進來
				// throw FileWatchServiceStopException (當呼叫 stopBusinessMontir()時)
				List<MonitoredFile> monitoredList =
							fileMonitor.readMonitoredList();
				if (!monitoredList.isEmpty()) {
					/*
					 *  將 MonitoredFile List 放入 Monitored File List Handler 中
					 *  ，讓 Handler 做處理。					
					 */
					this.monitoredFileListHandler.
											putElementInQueue(monitoredList);
				}				
			}			
		} catch (FileMonitorStopException e) {
			/*
			 * 拋出此例外表示已停止檔案監控。 
			 */
			
			// ********************** Log **********************
			if (IgxLog.messageLog.isDebugMode()) {
				IgxLog.messageLog.debug(this, "Detected file monitor has been stopped.");
			}
			// ********************** Log **********************
		}	
	}
	
	/**
	 * [Monitored File 重試管理]
	 * 當Queue內有要重試的Monitored File，newElementsArrive()方法會被觸發，並附
	 * 帶必須重試的數個Monitored File(放在List中)。 
	 * @author 洪政緯
	 *
	 */
	private final class RetryMonitoredFileHandler extends QueueService<MonitoredFile> {
		private RetryMonitoredFileHandler() {
			super("Retry Monitored File List Handler");
		}
		
		@Override
		protected void newElementArrive(MonitoredFile element) {
			List<MonitoredFile> retryMonitoredFileList = new LinkedList<MonitoredFile>();
			retryMonitoredFileList.add(element);
			// 從 super class 自行拿出  queue 內的全部 element，放入 List 中。
			synchronized(this) { // this 即表示此 QueueService
				while(!super.queue.isEmpty()) {
					retryMonitoredFileList.add(super.queue.poll());
				}
			}
			// 等待 retry 間隔時間
			RunTimeSystemUtils.threadSleep(retryInterval);
			// 放回 Monitored File List Handler 中				
			monitoredFileListHandler.putElementInQueue(retryMonitoredFileList);
		}
	}
	
	/*
	 * 將發生 IOException 之確認檔 (發生原因通常為檔案開啟失敗),
	 * 放入 retry handler 中 ， 如果失敗次數超過設定值，作對應的處理。
	 */
	private void handleIOExceptionMonitoredFile(MonitoredFile errorMonitoredFile) {
		if (retryTimesLimit > 0 &&
					errorMonitoredFile.retryTimes < retryTimesLimit) {
			/*
			 *  將發生 IOException 的 Monitored File 放入
			 *  Retry Monitored File Handler 中。
			 */
			errorMonitoredFile.retryTimes++;
			this.retryMonitorFileHandler.putElementInQueue(errorMonitoredFile);
		}
		else {
			// 超過重試次數，還是同樣發生 IO 錯誤
			
			File confirmationFile = errorMonitoredFile.getChangedFile();
			
			// ********************** Log **********************
			IgxLog.messageLog.error(this, "SUDA-270 Parsing incoming confirmation file '{}' has an I/O error.",
					confirmationFile.getName());
			// ********************** Log **********************
			
			this.physicalUnitBackup.removeAndBackupErrorUnit(confirmationFile);
		}
	}	


	/**
	 * [Monitored File List 處理監控]
	 * 若Queue內有要處理的 Monitored File List，則 newElementArrive()方法會被
	 * 觸發，並附帶上要處理的Monitored File List。	 
	 * @author 洪政緯
	 *
	 */
	private final class MonitoredFileListHandler
						extends QueueService<List<MonitoredFile>> {
		
		private MonitoredFileListHandler() {
			super("Monitored File List Handler");
		}
		
		@Override
		protected void newElementArrive(List<MonitoredFile> element) {
			// 將 List 內的 Monitored File 分别剖析成為 Business,再放入 List 中
			List<Unit> businessList = new LinkedList<Unit>();
			for(MonitoredFile monitoredFile : element) {
				/* 若 Monitor Event 為 Overflow ,表示檔案監控有遺失的情況發生。
				 * 此時的 MonitoredFile 內的 changedFile 為 null。
				 */
				if (monitoredFile.getFileChangedEvent().
						equals(FileMonitorEvent.EVENTOVERFLOW)) {
					// 檔案監控發生 overflow
					
					// ********************** Log **********************
					IgxLog.messageLog.error(SimpleUnitMonitor.this, "SUDA-271 Occured a file monitor overflow.");
					// ********************** Log **********************
					continue;
				}
				
				
				
				File confirmationFile = monitoredFile.getChangedFile();
				
				// 如果開起 '忽略 0KB 的檔案' 的功能
				if (ignoreEmptyFile) {
					/*
					 *  如果檔案大小為 0KB , 則忽略不做任何處理。
					 *  要有此功能主要是因為,PP 3G網路不穩定，時常會上傳 0KB 的確認檔上來，
					 *  所以有 0KB 則忽略，因為 PP 會自己再 retry。
					 */
					if (confirmationFile.length() <= 0) {
						// ********************** Log **********************
						IgxLog.messageLog.warn(SimpleUnitMonitor.this,
										"Ignored an empty(0KB) confirmation file '{}'.",
										confirmationFile.getAbsolutePath());
						// ********************** Log **********************
						continue;
					}
				}
				
				try {
					// ********************** Log **********************
					if (IgxLog.messageLog.isDebugMode()) {
						IgxLog.messageLog.debug(SimpleUnitMonitor.this, "Parsing incoming confirmation file '{}' (Retry:{})",
								confirmationFile.getName(), monitoredFile.retryTimes);
					}
					// ********************** Log **********************
					
					// 底層的 FileMonitor 如果是採用 JPatchWatchFileMonitor 需要做 Check 檔案是否傳輸完成。
					if (JPathWatchFileMonitor.class.isInstance(fileMonitor)) {
						// throws IOException
						IgxFileUtils.testFileLock(confirmationFile);
					}
					
					// 丟給 Unit Handler 去 Parse 成 Unit
					// throws ParseToUnitFormatException,ParseToUnitException
					Unit unit = 
							unitHandler.
									parseToUnit(confirmationFile);
					businessList.add(unit);
					
					// ********************** Log **********************
					if (IgxLog.messageLog.isInfoMode()) {
						IgxLog.messageLog.info(SimpleUnitMonitor.this, "Parsing incoming confirmation file '{}' completed.",
								confirmationFile.getName());
					}
					// ********************** Log **********************
				} catch (ParseToUnitFormatException e) {
					// 將檔案分析成為 Unit 時，格式錯誤
					
					// ********************** Log **********************
					IgxLog.messageLog.error(SimpleUnitMonitor.this, "SUDA-272 Parsing incoming confirmation file '{}' failed : {}"
							, confirmationFile.getName(), e.getMessage());
					// ********************** Log **********************
					
					// 將檔案移除並做備份
					physicalUnitBackup.removeAndBackupErrorUnit(confirmationFile);
				} catch (IOException e) {
					/*
					 *  Check 檔案是否傳輸完成時發生 IO 錯誤，
					 *  必須做重試處理。
					 */
					
					// ********************** Log **********************
					if (IgxLog.messageLog.isDebugMode()) {
						IgxLog.messageLog.debug(SimpleUnitMonitor.this, "Parsing incoming confirmation file '{}' has an I/O error.",
								confirmationFile.getName());
					}
					// ********************** Log **********************
					
					handleIOExceptionMonitoredFile(monitoredFile);
				} catch (ParseToUnitException e) {
					/*
					 *  將檔案分析成為 Unit 時，開檔發生 IO 錯誤，
					 *  必須做重試處理。
					 */
					
					// ********************** Log **********************
					if (IgxLog.messageLog.isDebugMode()) {
						IgxLog.messageLog.debug(SimpleUnitMonitor.this, "Parsing incoming confirmation file '{}' has an I/O error.",
								confirmationFile.getName());
					}
					// ********************** Log **********************
					
					handleIOExceptionMonitoredFile(monitoredFile);
				} catch (RedundantConfirmationFileException e) {
					/*
					 *  表示此確認檔為多餘的確認檔，無須轉換為Unit，
					 *  也不需要做錯誤處理（底層會做相關的處理）。
					 */
				}					
			}
			// 將 Business List 回傳.通知給 BusinessMonitorListener
			if (!businessList.isEmpty()) {
				unitMonitorListener.unitIncoming(businessList);
			}
		}
	}
	
	public static File getTerminalHaishinOutboxPath(String terminalId) {
		File path = new File(haishinOutboxPathStr,
							 terminalId);
		
		return path;
	}
	
	public static String getTerminalHaishinOutboxPathStr(String terminalId) {
		return
			getTerminalHaishinOutboxPath(terminalId).getAbsolutePath();
	}
	
	// --- getter and setter ---

	public void setPhysicalUnitBackup(UnitBackup physicalUnitBackup) {
		this.physicalUnitBackup = physicalUnitBackup;
	}
	
	public void setIgnoreEmptyFile(boolean ignoreEmptyFile) {
		this.ignoreEmptyFile = ignoreEmptyFile;
	}

	public void setRetryInterval(int retryInterval) {
		this.retryInterval = retryInterval;
	}

	public void setRetryTimesLimit(int retryTimesLimit) {
		this.retryTimesLimit = retryTimesLimit;
	}
	
	public void setHaishinOutboxPathStr(String haishinOutboxPathStr) {
		this.haishinOutboxPathStr = haishinOutboxPathStr;
	}
}
